package javafx.webview.demo.utils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebEvent;
import netscape.javascript.JSObject;

/**
 * 这是一个把java服务注册到javascript中的工具类
 * It also allows to call callback functions.
 * 
 * @author lipido
 */
public class Java2JavascriptUtils {
	/**
	 * 存储后端的所有服务对象
	 */
	private static Map<WebEngine, Map<String, Object>> 
		backendObjects = new HashMap<>();
	/**
	 * alert的监听器
	 */
	private static Set<WebEngine> 
		webEnginesWithAlertChangeListener =	new HashSet<>();
	
	private static boolean changing = false;
	
	/**
	 * 注册后台java对象成为javascript的变量.
	 * 把一个java对象连接到javascript中
	 * 一个特殊的alert()消息将会被执行
	 * "alert('__CONNECT__BACKEND__varname')" where varname is the javascript
	 * variable we want to make available.
	 * 
	 * The call to this function has to be performed before the engine loads the
	 * first page (where the alert call should take place).
	 * 
	 * @param webEngine 把java对象注册在哪个web引擎上
	 * @param varname 注册到java对象中的名字，使用的时候就是这个比如这个参数是 userServer，那么使用就是 userServer.add()
	 * @param backend 绑定到javascript引擎中的java对象，真正执行的是这个对象里面的方法.
	 */
	public static void connectBackendObject(
			final WebEngine webEngine,
			final String varname, 
			final Object backend) {
		
		registerBackendObject(webEngine, varname, backend);
		
		// 创建一个alert的监听器. 当我们想要监听一个alert的事件时
		//, 经过这个事件, javascript前后所有发送特殊alert的消息都会通过连接java后端对象
		//然后，如果这个进程仍然想设置它的alert在js引擎中
		// 我们需要创建一个 handlerwrapper

		
		// (*) It was impossible for me to re-connect the backend object
		// when the users navigates from one page to another page where the
		// backend object was also needed. The navigation erases any javascript
		// variables, so the backend has to be reconnected. However,
		// The recommended state change listeners on
		// webEngine were executed too late, after javascript code asking for the
		// backend object is executed, so it was not a solution.
		// The only way I found is to place a custom javacript "signaling" 
		// code to ask Java to reconnect the backend object.
		// The solution was "alert", because we can listen to alert calls from
		// javascript, so via an special "alert" message, we can connect the
		// backend object again.
		// It is not a bad solution, because the programmer has only to inlude
		// a simple additional script (such as "mybackend.js") in the page 
		// before any other scripts uses the backend variable.
		if (!webEnginesWithAlertChangeListener.contains(webEngine)) {//如果配置中的alert处理器中不存在这个web引擎
			if (webEngine.getOnAlert() == null) {
				webEngine.setOnAlert(new AlertEventHandlerWrapper(webEngine,
						null));
			}
			
			webEngine.onAlertProperty().addListener(
				new ChangeListener<EventHandler<WebEvent<String>>>() {

					@Override
					public void changed(ObservableValue<? extends EventHandler<WebEvent<String>>> arg0,
							EventHandler<WebEvent<String>> previous,
							final EventHandler<WebEvent<String>> newHandler) {
						//避免递归调用
						if (!changing) {
							changing = true;
							webEngine.setOnAlert(new AlertEventHandlerWrapper(webEngine, newHandler));
							changing = false;
						}
					}
			});
		}
		webEnginesWithAlertChangeListener.add(webEngine);
	}

	private static void registerBackendObject(final WebEngine webEngine,
			final String varname, final Object backend) {
		Map<String, Object> webEngineBridges = backendObjects.get(webEngine);
		if (webEngineBridges == null){
			webEngineBridges = new HashMap<>();
			backendObjects.put(webEngine, webEngineBridges);
			
		}
		webEngineBridges.put(varname, backend);
		
	}

    /**
     * 这是把js对象连接到后台java对象的方法
     * @param engine
     * @param varname
     */
	private static void connectToWebEngine(WebEngine engine, String varname) {
		if (backendObjects.containsKey(engine) && backendObjects.get(engine).containsKey(varname)) {
			JSObject window = (JSObject) engine.executeScript("window");
			window.setMember(varname, backendObjects.get(engine).get(varname));
		}
	}
	
	public static void call(Object callback, Object argument) {	
			// 它不是一个json对象, 所以使用它的方法来创建一个js对象
			((JSObject)callback).eval("this("+argument.toString()+")");
	}


    /**
     * alert的事件处理器
     */
	private final static class AlertEventHandlerWrapper implements EventHandler<WebEvent<String>> {

        /**
         * 连接后台的关键字
         */
		private static final String CONNECT_BACKEND_MAGIC_WORD = "__CONNECT__BACKEND__";
		private final EventHandler<WebEvent<String>> wrappedHandler;
		private WebEngine engine;

        /**
         * 提供的构造方法
         * @param engine
         * @param wrappedHandler
         */
		private AlertEventHandlerWrapper(WebEngine engine, EventHandler<WebEvent<String>> wrappedHandler) {
			this.engine = engine;
			this.wrappedHandler = wrappedHandler;
		}

        /**
         * 这个实现方法就是处理alert的代码
         * @param arg0
         */
		@Override
		public void handle(WebEvent<String> arg0) {
            if (arg0.getData().contains(CONNECT_BACKEND_MAGIC_WORD)) {
				String varname = arg0.getData().substring(CONNECT_BACKEND_MAGIC_WORD.length());
				connectToWebEngine(engine, varname);
			}else if(wrappedHandler != null){
				wrappedHandler.handle(arg0);
			}
		}
	}
}